﻿#region License
/***********************************************
 *  SearchEF v.1
 *  Written by James Levingston
 *  
 *  To get latest version or to see latest info:
 *  http://searchef.codeplex.com/
 *  
 *  The MIT License (MIT)
 *  http://en.wikipedia.org/wiki/MIT_License
 *  Copyright (C) <2011> by <James Levingston>
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the "Software"), to deal
 *  in the Software without restriction, including without limitation the rights
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the Software is
 *  furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in
 *  all copies or substantial portions of the Software.

 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *  THE SOFTWARE.
 ***/
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Reflection;
using System.Text.RegularExpressions;
using System.ComponentModel;
using System.Data.Services.Client;

namespace SearchEF
{
    /// <summary>
    /// Defines interface for an EFTextSearchEngine
    /// </summary>
    public interface IEFTextSearchEngine
    {
        List<ISearchResult<T>> SearchEntity<T>(string criteria) where T : class;
        List<ISearchResult<T>> SearchEntity<T>(string criteria, Func<T, bool> searchCriteria) where T : class;
        List<ISearchResult<T>> SearchEntity<T>(string criteria, Func<T, bool> searchCriteria, Func<Type, bool> typeCriteria) where T : class;
        List<ISearchResult<T>> SearchEntity<T>(string criteria, Func<T, bool> searchCriteria, Func<PropertyInfo, bool> propertyCriteria) where T : class;
        void SetContext(object context);
    }
    // <summary>
    /// Defines a generic interface for an EFTextSearchEngine
    /// </summary>
    public interface IEFTextSearchEngine<TContext> where TContext : DataServiceContext, new()
    {
        List<ISearchResult<T>> SearchEntity<T>(string criteria) where T : class;
        List<ISearchResult<T>> SearchEntity<T>(string criteria, Func<T, bool> searchCriteria) where T : class;
        List<ISearchResult<T>> SearchEntity<T>(string criteria, Func<T, bool> searchCriteria, Func<Type, bool> typeCriteria) where T : class;
        List<ISearchResult<T>> SearchEntity<T>(string criteria, Func<T, bool> searchCriteria, Func<PropertyInfo, bool> propertyCriteria) where T : class;
        void SetContext(TContext context);
    }
    /// <summary>
    /// Entity Framework Text Search Engine
    /// </summary>
    /// <typeparam name="TContext">EntityFramework Object Context</typeparam>
    public class EFTextSearchEngine<TContext> : BaseEFTextSearchEngine, IEFTextSearchEngine<TContext> where TContext : DataServiceContext, new()
    {
        protected IUnitOfWork<TContext> _work;

        #region Constructors
        public EFTextSearchEngine()
        {
            this._work = new UnitOfWork<TContext>();
        }
        public EFTextSearchEngine(TContext context)
        {
            this.SetContext(context);
        }
        public EFTextSearchEngine(IUnitOfWork<TContext> uOw)
        {
            this._work = uOw;
        }
        #endregion

        #region Public
        /// <summary>
        /// Sets TContext type
        /// </summary>
        /// <param name="context"></param>
        public void SetContext(TContext context)
        {
            this._work = new UnitOfWork<TContext>(context);
        }
        #endregion

        /// <summary>
        /// Get's a list of Entity Framework objects based on a where clause
        /// If no function is supplied - all records will be returned
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="whereClause"></param>
        /// <returns></returns>
        protected override List<TEntity> GetEntities<TEntity>(Func<TEntity, bool> whereClause)
        {
            //Check to see if we have a null function, if so, dont run one
            var runWhereClause = true;
            if (whereClause == default(Func<TEntity, bool>))
                runWhereClause = false;

            //Get all the types from the object context
            //if the property type is the one we're looking for
            //use a LINQ query to find it
            var result = new List<TEntity>();
            if (runWhereClause)
                result.AddRange(this._work.DataServiceContext.CreateQuery<TEntity>(typeof(TEntity).Name).Where(whereClause).ToList<TEntity>());
            else
                result.AddRange(this._work.DataServiceContext.CreateQuery<TEntity>(typeof(TEntity).Name).ToList<TEntity>());

            return result;
        }
    }
    /// <summary>
    /// Entity Framework Text Search Engine
    /// </summary>
    public class EFTextSearchEngine : BaseEFTextSearchEngine, IEFTextSearchEngine
    {
        protected IUnitOfWork _work { get; set; }

        #region Constructors
        public EFTextSearchEngine() { }
        public EFTextSearchEngine(object context)
        {
            this.SetContext(context);
        }
        public EFTextSearchEngine(IUnitOfWork uOw)
        {
            this._work = uOw;
        }
        /// <summary>
        /// Sets context type (note this should be typeof DataServiceContext
        /// </summary>
        /// <param name="context"></param>
        public void SetContext(object context)
        {
            this._work = new UnitOfWork(context);
        }
        #endregion

        /// <summary>
        /// Get's a list of Entity Framework objects based on a where clause
        /// If no function is supplied - all records will be returned
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="whereClause"></param>
        /// <returns></returns>
        protected override List<TEntity> GetEntities<TEntity>(Func<TEntity, bool> whereClause)
        {

            //Check to see if we have a null function, if so, dont run one
            var runWhereClause = true;
            if (whereClause == default(Func<TEntity, bool>))
                runWhereClause = false;

            //Get all the types from the object context
            //if the property type is the one we're looking for
            //use a LINQ query to find it
            var result = new List<TEntity>();
            if (runWhereClause)
                result.AddRange(this._work.DataServiceContext.CreateQuery<TEntity>(typeof(TEntity).Name).Where(whereClause).ToList<TEntity>());
            else
                result.AddRange(this._work.DataServiceContext.CreateQuery<TEntity>(typeof(TEntity).Name).ToList<TEntity>());

            return result;
        }
    }
    /// <summary>
    /// Base class where most of the search logic resides.
    /// Is responsible for parsing the search criteria. 
    /// Next, processing the contents of the criteria it into stack/queue of operands and operators.
    /// Then locating the correct match, correlating it, finally returning a list of ISearchResult<T> types.
    /// </summary>
    public abstract class BaseEFTextSearchEngine
    {
        #region Properties
        /// <summary>
        /// Sets the max length of information returned for each class property
        /// </summary>
        public int DisplayContentLength { get; set; }
        /// <summary>
        /// A function of types to process for each search
        /// </summary>
        protected Func<Type, bool> TypesToProcess { get; set; }
        protected Func<PropertyInfo, bool> PropertiesToProcess { get; set; }
        #endregion

        #region Public
        /// <summary>
        /// Search for an entity and get a list of search results of that type
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="criteria"></param>
        /// <returns></returns>
        public List<ISearchResult<T>> SearchEntity<T>(string criteria) where T : class
        {
            //Split the criteria up into items to search for
            var searchItems = this.SplitCriteriaInStack(criteria);
            //Check for operands
            var queue = this.ProcessStackIntoQueueList(searchItems);
            return GetResultsFromStack<T>(queue, null);
        }
        public List<ISearchResult<T>> SearchEntity<T>(string criteria, Func<T, bool> whereClause) where T : class
        {
            //Split the criteria up into items to search for
            var searchItems = this.SplitCriteriaInStack(criteria);
            //Check for operands
            var queue = this.ProcessStackIntoQueueList(searchItems);
            return GetResultsFromStack<T>(queue, whereClause);
        }
        public List<ISearchResult<T>> SearchEntity<T>(string criteria, Func<T, bool> whereClause, Func<Type, bool> typeCriteria) where T : class
        {
            //Set the types to process
            this.TypesToProcess = typeCriteria;
            //Split the criteria up into items to search for
            var searchItems = this.SplitCriteriaInStack(criteria);
            //Check for operands
            var queue = this.ProcessStackIntoQueueList(searchItems);
            return GetResultsFromStack<T>(queue, whereClause);
        }
        public List<ISearchResult<T>> SearchEntity<T>(string criteria, Func<T, bool> whereClause, Func<PropertyInfo, bool> propertyCriteria) where T : class
        {
            //Set the types to process
            this.PropertiesToProcess = propertyCriteria;
            //Split the criteria up into items to search for
            var searchItems = this.SplitCriteriaInStack(criteria);
            //Check for operands
            var queue = this.ProcessStackIntoQueueList(searchItems);
            return GetResultsFromStack<T>(queue, whereClause);
        }
        #endregion

        #region Protected
        /// <summary>
        /// Gets a list of nullable KeyValuePairs of type T and ISearchResult<T>
        /// based on a list of operands and function 'whereClause'
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="operands"></param>
        /// <param name="whereClause"></param>
        /// <returns></returns>
        protected virtual List<KeyValuePair<T, ISearchResult<T>>?> GetItems<T>(
            List<SearchOperand> operands,
            Func<T, bool> whereClause)
            where T : class
        {
            var items = new List<KeyValuePair<T, ISearchResult<T>>?>();
            //Get a list of LINQ entities and go through each of them
            //Then, for each item, get a Search Result
            this.GetEntities<T>(whereClause).ForEach(item =>
            {
                var result = this.GetResult<T>()(operands, item);
                items.AddRange(result);
            });
            return items;
        }
        /// <summary>
        /// Will be raised when the search critieria is split into a stack
        /// </summary>
        public event Action<Stack<string>> OnSplitCriteraInStack;
        /// <summary>
        /// Splits the criteria into a stack to be processed
        /// See OnSplitCriteraInStack to tune in to result or override
        /// </summary>
        /// <param name="criteria"></param>
        /// <returns></returns>
        protected virtual Stack<string> SplitCriteriaInStack(string criteria)
        {
            /************************************************
             * The goal here is to parse the search criteria into a stack 
             * that can be processed.
             * Each word in the search criteria will be seperated into an item on the stack
             * additionally, phrases will also be an item in the stack to be matched
             * ****/

            //Helpful snippet from:
            // http://stackoverflow.com/questions/554013/regular-expression-to-split-on-spaces-unless-in-quotes

            var regex = new Regex(@"((""((?<token>.*?)(?<!\\)"")|(?<token>[\w]+))(\s)*)", RegexOptions.None);

            //string input = @"   Here is ""my string"" it has   "" six  matches""   ";
            var i = (from Match m in regex.Matches(criteria)
                     where m.Groups["token"].Success
                     select m.Groups["token"].Value.Trim()).ToList();

            //Push items onto a stack
            var stack = new Stack<string>();
            i.ForEach(item => stack.Push(item));

            if (this.OnSplitCriteraInStack != null)
                OnSplitCriteraInStack(stack);

            return stack;
        }

        /// <summary>
        /// Will be raised when the stack is complete
        /// </summary>
        public event Action<List<SearchOperand>> OnProcessStackComplete;
        /// <summary>
        /// Processes the Stack into a list of logical operands
        /// </summary>
        /// <param name="requestedStack"></param>
        /// <returns></returns>
        protected virtual List<SearchOperand> ProcessStackIntoQueueList(Stack<string> requestedStack)
        {
            /**********************************************
             * The goal here is processs the stack and match operands to operators
             * This routine will handle -
             * AND, OR, NOT 
             * Invert the stack and pop items off once we try to match them
             * if there is no match, force the operand into an AND operator
             * ****/
            var oprtr = new Regex(@"\bOR|\bAND|\bNOT", RegexOptions.IgnoreCase);
            var queue = new List<SearchOperand>();

            //Invert the stack to process it easier
            requestedStack.Reverse<string>();

            //Keep looping until we run out
            while (requestedStack.Count > 0)
            {
                //Pop an item off the stack
                var oprd = requestedStack.Pop();
                //Match up the script
                var match = oprtr.Match(oprd);
                if (match.Length == 0)
                {
                    var operand = this.GetLogicalOperand(SearchOperatorEnum.AND, oprd);
                    if (requestedStack.Count > 0 && oprtr.Match(requestedStack.Peek()).Length > 0)
                        //Create the enum type, and pop it off the stack once we create the operator type
                        operand.OperatorType = (SearchOperatorEnum)Enum.Parse(typeof(SearchOperatorEnum), requestedStack.Pop().ToUpper());

                    queue.Add(operand);
                }
            }

            //Sort into a predifined order by the enum value
            var items = (from i in queue orderby (int)i.OperatorType select i).ToList();

            //Tell any observers we have completed and pass them our set our items to be processed
            if (this.OnProcessStackComplete != null)
                this.OnProcessStackComplete(items);

            return items;
        }
        /// <summary>
        /// Remove logical OR operators
        /// </summary>
        /// <param name="results"></param>
        protected virtual void RemoveLogicalOR<T>(List<ISearchResult<T>> results) where T : class
        {
            //Get a cound of AND - operator NOT was left behind
            //If there is no AND's then use an OR
            var logicalANDCount = (from i in results where i.OperatorType == SearchOperatorEnum.AND select i).Count();
            if (logicalANDCount > 0)
                results.RemoveAll(item => item.OperatorType == SearchOperatorEnum.OR);
        }
        /// <summary>
        /// Returns an instance of ISearchResult<T>.
        /// This method is overridable. Passing in the map function
        /// is helpful when invoking custom ISearchResult<T> 
        /// types or remapping the current values
        /// </summary>
        /// <typeparam name="T">Search type</typeparam>
        /// <param name="entity">type located</param>
        /// <param name="correlation">corrleation of the search result to the content of the entity</param>
        /// <param name="oprtr">operator used</param>
        /// <param name="pattern">search pattern</param>
        /// <param name="content">content ot the entity</param>
        /// <param name="map">third party mapping function</param>
        /// <returns></returns>
        protected virtual ISearchResult<T> GetSearchResult<T>(
            T entity,
            MatchCorrelation correlation,
            SearchOperatorEnum oprtr,
            string pattern,
            string content,
            Func<ISearchResult<T>, ISearchResult<T>> map)
            where T : class
        {

            //Trim the content
            this.TrimContent<T>(content);

            //create the result, we're finally done!
            var result = new SearchResult<T>
            {
                Content = content ?? string.Empty,
                Correlation = correlation,
                ResultType = entity.GetType(),
                ResultObject = entity,
                OperatorType = oprtr,
                Operand = pattern
            };

            //if there is no mapping routine, 
            //then just return
            if (map == null)
                return result;
            else
                return map(result);
        }

        #endregion

        #region Private
        /// <summary>
        /// Trim content
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="entity"></param>
        /// <param name="content"></param>
        private void TrimContent<T>(string content)
        {
            if (this.DisplayContentLength == 0)
                return;

            //This is supposed to trim the length of each property based on
            //the global property - DisplayContentLength
            if (content != null)
            {
                var builder = new System.Text.StringBuilder();
                content.Split('|').ToList().ForEach(item =>
                {
                    if (item.Length > this.DisplayContentLength)
                        builder.Append(item.Substring(0, this.DisplayContentLength - 1) + "...| ");
                    else if (item.Trim().Length == 0)
                        return; //Do nothing, skip
                    else
                        builder.Append(item + " | ");
                });

                //Remove the last character, the last delimiter
                if (builder.Length > 0)
                    content = builder.ToString().Remove(builder.ToString().Length - 2);
            }

        }
        /// <summary>
        /// Gets the a list of results from a list of operands, passing an function 'where' clause
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="operands"></param>
        /// <param name="whereClause"></param>
        /// <returns></returns>
        private List<ISearchResult<T>> GetResultsFromStack<T>(List<SearchOperand> operands, Func<T, bool> whereClause) where T : class
        {
            //Setup the results
            var results = new List<ISearchResult<T>>();

            //Get the result items
            var items = this.GetItems<T>(operands, whereClause);

            //Load the results from the items
            items.ForEach(item =>
            {
                if (item.HasValue)
                    results.Add(item.Value.Value);
            });

            //Remove any nulls
            results.RemoveAll(item => item == null);

            //Remove any logical OR's
            this.RemoveLogicalOR(results);

            return results;
        }
        /// <summary>
        /// Get's a list of Entity Framework objects based on a where clause
        /// This method must be overridden, this is where you design you're own LINQ statement against entities
        /// If no 'whereclause' function is supplied - all records will be returned
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="whereClause"></param>
        /// <returns></returns>
        protected virtual List<TEntity> GetEntities<TEntity>(Func<TEntity, bool> whereClause) where TEntity : class
        {
            throw new NotImplementedException("Please override this method");
        }
        /// <summary>
        /// Gets a logical operand type
        /// </summary>
        /// <param name="operatorType"></param>
        /// <param name="operand"></param>
        /// <returns></returns>
        private SearchOperand GetLogicalOperand(SearchOperatorEnum operatorType, string operand)
        {
            return new SearchOperand { OperatorType = operatorType, Operand = operand.ToUpper() };
        }
        /// <summary>
        /// Raised when a result is found
        /// </summary>
        public event Action<ISearchResult> OnResultFound;
        /// <summary>
        /// Creates a function from type T
        /// </summary>
        /// <typeparam name="T">An Entity Object type</typeparam>
        /// <returns>A function for faster exection</returns>
        private Func<List<SearchOperand>, T, List<KeyValuePair<T, ISearchResult<T>>?>> GetResult<T>() where T : class
        {
            //A function is returned here for faster execution
            return (List<SearchOperand> operands, T item) =>
            {
                //Setup
                var endLoop = false;
                var isNotMatch = false;
                var results = new List<KeyValuePair<T, ISearchResult<T>>?>();
                var result = default(KeyValuePair<T, ISearchResult<T>>?);
                /*********************************************************
                 * The goal here is to go through each property and perform a regex search
                 * perform, if there is matches - check to see if that match was NOT to be included in the result.
                 * If the an operand is matched to EF content, but has an operator type of NOT - ignore it
                 *********************************************************/

                //First - transform T's property values into a pipe delimted string to make it easy to search
                var vals = this.ToPipeDelimitedString(item);
                if (string.IsNullOrEmpty(vals))
                    return results;

                //Next, loop through all the operands to find a match
                //While looping, perform the match until we find a match.
                //NOT logic is handled here
                operands.ForEach(operand =>
                {
                    while (endLoop == false)
                    {
                        //Search exact word - no 'contains' type search
                        var oprtr = new Regex(@"\b" + operand.Operand, RegexOptions.IgnoreCase);
                        var match = oprtr.Match(vals);

                        //If we have match, then peform the NOT check
                        //If the operator is NOT, then we're going to ignore it
                        if (match.Length > 0)
                        {
                            if (operand.OperatorType != SearchOperatorEnum.NOT)
                            {
                                //If we find a match, end the loop
                                result = new KeyValuePair<T, ISearchResult<T>>(
                                    item,
                                    this.GetSearchResult<T>(item, this.GetMatchCorrelation(match.Length), operand.OperatorType, operand.Operand, vals, null)
                                    );
                                endLoop = true;

                                //Raise the event and notify an observer, we have a result
                                if (result != null)
                                {
                                    results.Add(result);
                                    if (this.OnResultFound != null)
                                        this.OnResultFound(result.Value.Value);
                                }
                            }
                            else //If this is a 'NOT', then we ignore the match and end the loop
                            {
                                isNotMatch = true;
                                endLoop = true;
                            }
                        }
                        else //There was nothing matched for that operator
                            endLoop = true;
                    }
                    //Reset the flag
                    endLoop = false;

                    if (isNotMatch) //If we matched a 'NOT' then there is no need to continue processing back into the loop
                        endLoop = true;
                });

                return results;
            };
        }
        /// <summary>
        /// Evaluates each property of the object and adds it to a '|' delimited string
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        private string ToPipeDelimitedString(object obj)
        {
            if (obj == null)
                return null;

            var builder = new System.Text.StringBuilder();
            var props = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
            var availableTypes = this.GetTypes();

            //Loop the through the properties, we only want to eval 
            //the below types
            for (int p = 0; p < props.Length; p++)
            {
                //If there is no function, then it's true, else run the properties to process function
                if (this.PropertiesToProcess == null ? true : this.PropertiesToProcess(props[p]))
                {
                    var isDefinedType = default(Type);

                    //Check for the class level function, if none if provided check to see if it matches the list
                    if (this.TypesToProcess == null)
                        isDefinedType = availableTypes.Where(t => t == props[p].PropertyType).FirstOrDefault();
                    else //run the the function defined be the user, if it matchces an item on the list
                        isDefinedType = availableTypes.Where(t => TypesToProcess(props[p].PropertyType)).FirstOrDefault();

                    if (isDefinedType != null)
                    {
                        var val = Convert.ToString(props[p].GetValue(obj, null));
                        if (!string.IsNullOrEmpty(val))
                            builder.Append(val + "|");
                    }
                }
            }

            //Remove the last character, the last delimiter
            if (builder.Length > 0)
                return builder.ToString().Remove(builder.ToString().Length - 1);

            return null;
        }
        /// <summary>
        /// Gets a list of types that are processed during the search engine
        /// </summary>
        /// <returns></returns>
        protected virtual List<Type> GetTypes()
        {
            //TODO: Add future types to eval here
            return new List<Type>
            {
                typeof(bool),
                typeof(string),
                typeof(DateTime),
                typeof(Guid),
                typeof(decimal),
                typeof(double),
                typeof(float),
                typeof(long),
                typeof(ulong),
                typeof(int),
                typeof(uint),
                typeof(short),
                typeof(ushort),
                typeof(byte),
                typeof(sbyte),
                typeof(Nullable<DateTime>),
                typeof(Nullable<Guid>),
                typeof(Nullable<decimal>),
                typeof(Nullable<double>),
                typeof(Nullable<float>),
                typeof(Nullable<long>),
                typeof(Nullable<ulong>),
                typeof(Nullable<int>),
                typeof(Nullable<uint>),
                typeof(Nullable<short>),
                typeof(Nullable<ushort>),
                typeof(Nullable<byte>),
                typeof(Nullable<sbyte>),
                typeof(Nullable<bool>)
            };
        }
        /// <summary>
        /// Matches the correlation to a count
        /// </summary>
        /// <param name="count"></param>
        /// <returns></returns>
        private MatchCorrelation GetMatchCorrelation(int count)
        {
            if (count == 1)
                return MatchCorrelation.Exact;
            if (count > 1 && count <= 25)
                return MatchCorrelation.High;
            if (count > 25 && count <= 50)
                return MatchCorrelation.Medium;
            if (count > 50)
                return MatchCorrelation.Low;

            return MatchCorrelation.Zero;
        }
        #endregion
    }
}
